﻿using System;
using System.Linq;
using System.Reflection;
using Castle.Components.Binder;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Metadata;
using Yama.MVC.NHibernate.Connections;

namespace Yama.MVC.NHibernate.DataBinder
{
    public class NhibernateDataBinder : Castle.Components.Binder.DataBinder
    {
        private readonly ISessionProvider _provider;

        public NhibernateDataBinder(ISessionProvider provider)
        {
            _provider = provider;
        }

        private ISession Session { get { return _provider.CurrentSession; } }

        private bool IsPersistentType(Type parameterType)
        {
            return GetClassMetadata(parameterType) != null;
        }

        private IClassMetadata GetClassMetadata(Type parameterType)
        {
            return Session.SessionFactory.GetClassMetadata(parameterType);
        }

        private object GetPersistentInstance(Type type, object primaryKey, bool fetchByLoad)
        {
            if (fetchByLoad)
                return Session.Load(type, primaryKey);

            return Session.Get(type, primaryKey);
        }

        public object BindPersistentObject(Type instanceType, string paramPrefix, CompositeNode node, bool fetchByLoad, bool nonFlatNodes)
        {
            if (IsPersistentType(instanceType))
            {
                return BindObject(instanceType, paramPrefix, node, fetchByLoad);
            }
            if (instanceType.IsArray && instanceType.HasElementType && IsPersistentType(instanceType.GetElementType()))
            {
                return nonFlatNodes
                    ? BindArrayFromNonFlatNode(instanceType.GetElementType(), paramPrefix, node, fetchByLoad)
                    : BindArrayFromFlatNode(instanceType.GetElementType(), paramPrefix, node, fetchByLoad);
            }

            return BindObject(instanceType, paramPrefix, node);
        }

        private object BindArrayFromFlatNode(Type instanceType, string paramPrefix, CompositeNode node, bool fetchByLoad)
        {
            Type primaryKeyType = GetClassMetadata(instanceType).IdentifierType.ReturnedClass;

            // bind object ids using DataBinder
            Array arrayOfIds = (Array)BindObject(primaryKeyType.MakeArrayType(), paramPrefix, node);

            return GetPersistentObjectsFromPrimaryKeysArray(instanceType, arrayOfIds, fetchByLoad);
        }

        private object BindArrayFromNonFlatNode(Type instanceType, string paramPrefix, CompositeNode node, bool fetchByLoad)
        {
            // bind objects array using DataBinder
            object[] arrayOfEmptyObjects = (object[])BindObject(instanceType.MakeArrayType(), paramPrefix, node);

            // fetch initialized objects from database
            return GetPersistentObjectsFromArray(instanceType, arrayOfEmptyObjects, fetchByLoad);
        }

        private object BindObject(Type instanceType, string paramPrefix, CompositeNode node, bool fetchByLoad)
        {
            // get primary key property and bind it using base Binder
            Type primaryKeyType = GetClassMetadata(instanceType).IdentifierType.ReturnedClass;
            object primaryKey = BindParameter(primaryKeyType, paramPrefix, node);

            return primaryKey == null ? null : GetPersistentInstance(instanceType, primaryKey, fetchByLoad);
        }

        private object GetPersistentObjectsFromArray(Type instanceType, object[] arrayOfEmptyObjects, bool fetchByLoad)
        {
            Array outputArr = Array.CreateInstance(instanceType, arrayOfEmptyObjects.Length);
            string idProperty = GetClassMetadata(instanceType).IdentifierPropertyName;
            PropertyInfo primaryKeyProperty = instanceType.GetProperty(idProperty);

            if (fetchByLoad)
            {
                for (int i = 0; i < arrayOfEmptyObjects.Length; i++)
                {
                    var unInititalized = arrayOfEmptyObjects[i];
                    object initialized = Session.Load(instanceType, primaryKeyProperty.GetValue(unInititalized, null));

                    outputArr.SetValue(initialized, i);
                }
            }
            else
            {
                var ids = from emptyObject in arrayOfEmptyObjects
                          select primaryKeyProperty.GetValue(emptyObject, null);

                var objects = Session.CreateCriteria(instanceType)
                    .Add(Restrictions.In(idProperty, ids.ToArray()))
                    .List();

                for (int i = 0; i < objects.Count; i++)
                {
                    var o = objects[i];
                    outputArr.SetValue(o, i);
                }
            }

            return outputArr;
        }

        private object GetPersistentObjectsFromPrimaryKeysArray(Type instanceType, Array arrayOfIds, bool fetchByLoad)
        {
            Array outputArr = Array.CreateInstance(instanceType, arrayOfIds.Length);
            string idProperty = GetClassMetadata(instanceType).IdentifierPropertyName;

            if (fetchByLoad)
            {
                var enumerator = arrayOfIds.GetEnumerator();

                int i = 0;
                while(enumerator.MoveNext())
                {
                    object proxy = Session.Load(instanceType, enumerator.Current);
                    outputArr.SetValue(proxy, i++);
                }
            }
            else
            {
                var objects = Session.CreateCriteria(instanceType)
                    .Add(Restrictions.In(idProperty, arrayOfIds))
                    .List();

                for (int i = 0; i < objects.Count; i++)
                {
                    var o = objects[i];
                    outputArr.SetValue(o, i);
                }
            }

            return outputArr;
        }
    }
}